/*-
 * ========================LICENSE_START=================================
 * restheart-commons
 * %%
 * Copyright (C) 2019 - 2025 SoftInstigate
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * =========================LICENSE_END==================================
 */
package org.restheart.configuration;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.commons.jxpath.JXPathContext;
import org.restheart.configuration.Utils.RhOverride;
import static org.restheart.configuration.Utils.asMap;
import static org.restheart.configuration.Utils.findOrDefault;
import static org.restheart.configuration.Utils.overrides;
import org.restheart.utils.BootstrapLogger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;

import com.google.common.collect.Maps;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.Strictness;
import com.mongodb.ConnectionString;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;

/**
 * Main configuration class that holds all RESTHeart configuration settings.
 * 
 * <p>This class is responsible for loading configuration from YAML or JSON files,
 * applying environment variable overrides, and providing access to all configuration
 * settings. Configuration is immutable once loaded.</p>
 * 
 * <h2>Configuration Loading</h2>
 * <p>Configuration can be loaded from:</p>
 * <ul>
 *   <li>YAML files using {@code Configuration.Builder.build(Path, boolean)}</li>
 *   <li>JSON files using {@code Configuration.Builder.build(Path, boolean)}</li>
 *   <li>Map objects using {@code Configuration.Builder.build(Map, Path, boolean)}</li>
 * </ul>
 * 
 * <h2>Environment Variable Overrides</h2>
 * <p>Configuration values can be overridden using environment variables with the
 * pattern {@code RH_<SECTION>_<KEY>}. For example:</p>
 * <ul>
 *   <li>{@code RH_CORE_NAME} overrides {@code core.name}</li>
 *   <li>{@code RH_HTTP_LISTENER_PORT} overrides {@code http-listener.port}</li>
 * </ul>
 * 
 * <h2>Default Values</h2>
 * <p>If configuration properties are not specified, the following defaults are used:</p>
 * <ul>
 *   <li>HTTP listener: enabled on localhost:8080</li>
 *   <li>HTTPS listener: disabled on localhost:4443</li>
 *   <li>AJP listener: disabled on localhost:8009</li>
 * </ul>
 * 
 * @author Andrea Di Cesare {@literal <andrea@softinstigate.com>}
 * @since 1.0
 */
public class Configuration {

    private static final String LOG_PATTERN = "\t{} -> {}";

    private static final String MASK = "**********";

    private static final String LOCALHOST = "localhost";

    /**
     * RESTHeart version string.
     * 
     * <p>The version is read from the JAR's MANIFEST.MF file, which is automatically
     * generated by the Maven build process. Returns "unknown, not packaged" when
     * running from IDE or unpackaged classes.</p>
     */
    public static final String VERSION = Configuration.class.getPackage().getImplementationVersion() == null
            ? "unknown, not packaged"
            : Configuration.class.getPackage().getImplementationVersion();

    static final Logger LOGGER = LoggerFactory.getLogger(Configuration.class);

    public static final String DEFAULT_ROUTE = "0.0.0.0";

    /**
     * Path to the configuration file that was loaded.
     * 
     * <p>This is set when configuration is loaded from a file and can be retrieved
     * using {@link #getPath()}. It is {@code null} if configuration was loaded
     * from a Map or other source.</p>
     */
    private static Path PATH = null;

    private static final Listener DEFAULT_HTTP_LISTENER = new Listener(true, LOCALHOST, 8080);
    private static final TLSListener DEFAULT_HTTPS_LISTENER = new TLSListener(false, LOCALHOST, 4443, null, null, null);
    private static final Listener DEFAULT_AJP_LISTENER = new Listener(false, LOCALHOST, 8009);

    /**
     * Configuration key for Undertow connection options.
     * 
     * <p>These options configure the underlying Undertow server behavior including
     * buffer sizes, timeouts, and other low-level settings.</p>
     * 
     * @see <a href="http://undertow.io/undertow-docs/undertow-docs-1.3.0/index.html#common-listener-options">
     *      Undertow Common Listener Options</a>
     */
    public static final String CONNECTION_OPTIONS_KEY = "connection-options";

    private final Listener httpListener;
    private final Listener ajpListener;
    private final TLSListener httpsListener;
    private final List<ProxiedResource> proxies;
    private final List<StaticResource> staticResources;
    private final CoreModule coreModule;
    private final Logging logging;
    private final Map<String, Object> connectionOptions;

    private final Map<String, Object> conf;

    /**
     * Creates a new instance of Configuration from the provided configuration map.
     * 
     * <p>For any missing property, the default value is used. This constructor is
     * private and should be accessed through the {@link Builder} class.</p>
     *
     * @param conf the key-value configuration map containing all settings
     * @param confFilePath the path to the configuration file (can be null)
     * @param silent if true, suppresses warning messages for missing optional properties
     * @throws ConfigurationException if required configuration properties are missing
     *         or invalid
     */
    private Configuration(final Map<String, Object> conf, final Path confFilePath, final boolean silent)
            throws ConfigurationException {
        PATH = confFilePath;

        this.conf = conf;

        this.coreModule = CoreModule.build(conf, silent);

        if (findOrDefault(conf, Listener.HTTP_LISTENER_KEY, null, true) != null) {
            httpListener = new Listener(conf, Listener.HTTP_LISTENER_KEY, DEFAULT_HTTP_LISTENER, silent);
        } else {
            httpListener = DEFAULT_HTTP_LISTENER;
        }

        if (findOrDefault(conf, TLSListener.HTTPS_LISTENER_KEY, null, true) != null) {
            httpsListener = new TLSListener(conf, TLSListener.HTTPS_LISTENER_KEY, DEFAULT_HTTPS_LISTENER, silent);
        } else {
            httpsListener = DEFAULT_HTTPS_LISTENER;
        }

        if (findOrDefault(conf, Listener.AJP_LISTENER_KEY, null, true) != null) {
            ajpListener = new Listener(conf, Listener.AJP_LISTENER_KEY, DEFAULT_AJP_LISTENER, silent);
        } else {
            ajpListener = DEFAULT_AJP_LISTENER;
        }

        proxies = ProxiedResource.build(conf, silent);

        staticResources = StaticResource.build(conf, silent);

        logging = Logging.build(conf, silent);

        connectionOptions = asMap(conf, CONNECTION_OPTIONS_KEY, null, silent);
    }

    /**
     * Returns a YAML representation of the configuration.
     * 
     * <p>The output is formatted with proper indentation and flow style for
     * readability. This is useful for debugging and logging the current
     * configuration state.</p>
     * 
     * @return YAML string representation of the configuration
     */
    @Override
    public String toString() {
        final var dumpOpts = new DumperOptions();
        dumpOpts.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
        dumpOpts.setPrettyFlow(true);
        dumpOpts.setIndent(2);
        dumpOpts.setCanonical(false);
        dumpOpts.setExplicitStart(true);

        final var sw = new StringWriter();
        new Yaml(dumpOpts).dump(conf, sw);

        return sw.toString();
    }

    /**
     * Gets a configuration value by key with a default fallback.
     * 
     * <p>The key can use path notation to access nested values, for example:
     * "core.name" or "http-listener.port".</p>
     * 
     * @param <V> the type of the value
     * @param key the configuration key using dot notation for nested values
     * @param defaultValue the default value to return if the key is not found
     * @return the configuration value or defaultValue if not found
     */
    public <V extends Object> V getOrDefault(final String key, final V defaultValue) {
        return Utils.getOrDefault(this, key, defaultValue, true);
    }

    /**
     * Returns an unmodifiable view of the configuration as a Map.
     * 
     * <p>This map contains all configuration settings in their raw form.
     * Modifications to the returned map are not allowed and will throw
     * {@link UnsupportedOperationException}.</p>
     * 
     * @return unmodifiable map of all configuration settings
     */
    public Map<String, Object> toMap() {
        return Collections.unmodifiableMap(this.conf);
    }

    /**
     * Gets the core module configuration.
     * 
     * <p>The core module contains essential settings like instance name,
     * plugin directories, thread pools, and buffer configuration.</p>
     * 
     * @return the core module configuration
     */
    public CoreModule coreModule() {
        return coreModule;
    }

    /**
     * Gets the list of configured proxied resources.
     * 
     * <p>Proxied resources define reverse proxy configurations that forward
     * requests to backend services. The returned list is unmodifiable.</p>
     * 
     * @return unmodifiable list of proxied resource configurations
     */
    public List<ProxiedResource> getProxies() {
        return Collections.unmodifiableList(proxies);
    }

    /**
     * Gets the list of configured static resources.
     * 
     * <p>Static resources define mappings for serving static files like HTML,
     * CSS, JavaScript, and images. The returned list is unmodifiable.</p>
     * 
     * @return unmodifiable list of static resource configurations
     */
    public List<StaticResource> getStaticResources() {
        return Collections.unmodifiableList(staticResources);
    }

    /**
     * Gets the HTTP listener configuration.
     * 
     * <p>The HTTP listener handles plain HTTP requests. By default, it is
     * enabled on localhost:8080.</p>
     * 
     * @return the HTTP listener configuration
     */
    public Listener httpListener() {
        return httpListener;
    }

    /**
     * Gets the HTTPS listener configuration.
     * 
     * <p>The HTTPS listener handles secure HTTPS requests with TLS/SSL.
     * By default, it is disabled but configured for localhost:4443.</p>
     * 
     * @return the HTTPS listener configuration with TLS settings
     */
    public TLSListener httpsListener() {
        return httpsListener;
    }

    /**
     * Gets the AJP listener configuration.
     * 
     * <p>The AJP (Apache JServ Protocol) listener enables integration with
     * web servers like Apache HTTP Server. By default, it is disabled but
     * configured for localhost:8009.</p>
     * 
     * @return the AJP listener configuration
     */
    public Listener ajpListener() {
        return ajpListener;
    }

    /**
     * Gets the effective log level.
     * 
     * <p>If a Logback configuration file is specified via system property
     * {@code logback.configurationFile}, the log level is read from the
     * Logback context. Otherwise, it returns the level from the configuration
     * file.</p>
     * 
     * @return the current log level for org.restheart package
     */
    public Level getLogLevel() {
        final var logbackConfigurationFile = System.getProperty("logback.configurationFile");
        if (logbackConfigurationFile != null && !logbackConfigurationFile.isEmpty()) {
            final var loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
            final var logger = loggerContext.getLogger("org.restheart");
            return logger.getLevel();
        }

        return logging.logLevel();
    }

    /**
     * Gets the logging configuration.
     * 
     * <p>The logging configuration includes log level, log file settings,
     * and whether to enable metrics logging.</p>
     * 
     * @return the logging configuration
     */
    public Logging logging() {
        return logging;
    }

    /**
     * Gets the Undertow connection options.
     * 
     * <p>Connection options configure low-level Undertow server settings such as
     * buffer sizes, timeouts, and HTTP/2 settings. The returned map is unmodifiable.</p>
     * 
     * @return unmodifiable map of connection options
     * @see <a href="http://undertow.io/undertow-docs/undertow-docs-1.3.0/index.html#common-listener-options">
     *      Undertow Documentation</a>
     */
    public Map<String, Object> getConnectionOptions() {
        return Collections.unmodifiableMap(connectionOptions);
    }

    /**
     * Gets the path of the configuration file that was loaded.
     * 
     * <p>This returns the path that was used to load the configuration,
     * or {@code null} if the configuration was loaded from a Map or
     * other non-file source.</p>
     * 
     * @return the path of the configuration file, or null if not loaded from a file
     */
    public static Path getPath() {
        return PATH;
    }

    /**
     * Builder class for creating Configuration instances.
     * 
     * <p>This class provides factory methods for building Configuration objects from
     * various sources including files, streams, and default configurations. It handles
     * both YAML and JSON formats and supports configuration overrides through environment
     * variables and override files.</p>
     * 
     * <h3>Configuration Sources</h3>
     * <ul>
     *   <li>Default embedded configuration files</li>
     *   <li>External YAML or JSON configuration files</li>
     *   <li>Configuration override files in YAML, JSON, or RHO format</li>
     *   <li>Environment variables (RH_* and RHO patterns)</li>
     * </ul>
     * 
     * <h3>Override Formats</h3>
     * <ul>
     *   <li>YAML files (.yml, .yaml)</li>
     *   <li>JSON files (.json, .jsonc)</li>
     *   <li>RHO format files (.conf) - simple key=value format</li>
     *   <li>RHO environment variable - semicolon-separated key=value pairs</li>
     * </ul>
     * 
     * @since 1.0
     */
    public class Builder {

        private Builder() {
        }

        /**
         * Builds a Configuration using the default embedded configuration file.
         * 
         * <p>Uses either the standalone configuration (without MongoDB) or the
         * standard configuration based on the {@code standaloneConfiguration} parameter.</p>
         *
         * @param standaloneConfiguration if true, uses configuration without MongoDB settings;
         *                                if false, uses standard configuration with MongoDB
         * @param silent if true, suppresses warning messages for missing optional properties
         * @return the default configuration instance
         * @throws ConfigurationException if the default configuration cannot be loaded
         */
        public static Configuration build(final boolean standaloneConfiguration, final boolean silent) {
            return build(null, null, standaloneConfiguration, silent);
        }

        /**
         * Builds a Configuration from specified file paths with optional overrides.
         * 
         * <p>If {@code confFilePath} is null, loads the appropriate default configuration.
         * Configuration can be overridden using an override file and environment variables.</p>
         * 
         * <h4>Override File Formats</h4>
         * <ul>
         *   <li>.yml/.yaml - YAML format</li>
         *   <li>.json/.jsonc - JSON format with optional comments</li>
         *   <li>.conf - RHO format (key=value pairs)</li>
         * </ul>
         * 
         * <h4>Environment Variable Overrides</h4>
         * <p>After loading the configuration and applying file overrides, environment
         * variables are processed:</p>
         * <ul>
         *   <li>RHO - semicolon-separated key=value pairs</li>
         *   <li>RH_* - individual configuration keys (e.g., RH_CORE_NAME)</li>
         * </ul>
         *
         * @param confFilePath path to the main configuration file (null for default)
         * @param confOverridesFilePath optional path to override file (can be null)
         * @param standaloneConfiguration if true and confFilePath is null, uses standalone defaults
         * @param silent if true, suppresses warning messages for missing optional properties
         * @return configured Configuration instance with all overrides applied
         * @throws ConfigurationException if configuration files cannot be read or are invalid
         */
        public static Configuration build(final Path confFilePath, final Path confOverridesFilePath,
                final boolean standaloneConfiguration, final boolean silent) throws ConfigurationException {
            if (confFilePath == null) {
                final var defaultConfFilePath = standaloneConfiguration ? "/restheart-default-config-no-mongodb.yml"
                        : "/restheart-default-config.yml";
                final var stream = Configuration.class.getResourceAsStream(defaultConfFilePath);
                try (var confReader = new InputStreamReader(stream)) {
                    return build(confReader, null, confOverridesFilePath, silent);
                } catch (final IOException ieo) {
                    throw new ConfigurationException("Error reading default configuration file", ieo);
                }
            } else {
                try (var confReader = new BufferedReader(new FileReader(confFilePath.toFile()))) {
                    return build(confReader, confFilePath, confOverridesFilePath, silent);
                } catch (final FileNotFoundException ex) {
                    throw new ConfigurationException("Configuration file not found: " + confFilePath, ex, false);
                } catch (final IOException ieo) {
                    throw new ConfigurationException("Error reading configuration file " + confFilePath, ieo);
                }
            }
        }

        /**
         * Builds a Configuration from a Reader with optional override file.
         * 
         * <p>This is the core build method that:</p>
         * <ol>
         *   <li>Parses the main configuration from the Reader (YAML format)</li>
         *   <li>Applies overrides from the override file if provided</li>
         *   <li>Applies environment variable overrides (RHO and RH_* patterns)</li>
         *   <li>Creates the final Configuration instance</li>
         * </ol>
         *
         * @param confReader reader for the main configuration content (YAML format)
         * @param confFilePath optional path reference for the configuration file
         * @param confOverridesFilePath optional path to override file
         * @param silent if true, suppresses warning messages
         * @return configured Configuration instance
         * @throws ConfigurationException if configuration is invalid or cannot be parsed
         */
        private static Configuration build(final Reader confReader, final Path confFilePath, final Path confOverridesFilePath,
                final boolean silent) throws ConfigurationException {
            Map<String, Object> confMap = new Yaml(new SafeConstructor(new LoaderOptions())).load(confReader);

            if (confOverridesFilePath != null) {
                try {
                    String overrides;

                    if (confOverridesFilePath.toString().toLowerCase().endsWith(".yml")
                            || confOverridesFilePath.toString().toLowerCase().endsWith(".yaml")) {
                        // YML format
                        try {
                            overrides = fromYmlToRho(Files.newBufferedReader(confOverridesFilePath));
                        } catch (final JsonParseException jpe) {
                            throw new ConfigurationException(
                                    "Wrong configuration override YML file: " + jpe.getLocalizedMessage(), jpe, false);
                        }
                    } else if (confOverridesFilePath.toString().toLowerCase().endsWith(".json")
                            || confOverridesFilePath.toString().toLowerCase().endsWith(".jsonc")) {
                        // JSON format
                        try {
                            overrides = fromJsonToRho(Files.newBufferedReader(confOverridesFilePath));
                        } catch (final JsonParseException jpe) {
                            throw new ConfigurationException(
                                    "Wrong configuration override JSON file: " + jpe.getLocalizedMessage(), jpe, false);
                        }
                    } else if (confOverridesFilePath.toString().toLowerCase().endsWith(".conf")) {
                        // RHO format
                        overrides = Files.readAllLines(confOverridesFilePath).stream()
                                .filter(row -> !row.strip().startsWith("#")) // ingore comments lines
                                .collect(Collectors.joining());
                    } else {
                        throw new ConfigurationException(
                                "Configuration override file must have .json, .jsonc, .yml, .yaml or .conf extension: "
                                        + confOverridesFilePath);
                    }

                    if (!silent) {
                        BootstrapLogger.standalone(LOGGER, "Overriding configuration from file: {}", confOverridesFilePath);
                    }

                    confMap = overrideConfiguration(confMap, overrides(overrides, true, silent), silent);
                } catch (final IOException ioe) {
                    throw new ConfigurationException("Configuration override file not found: " + confOverridesFilePath,
                            ioe, false);
                }
            }

            // overrides with RHO env var
            if (System.getenv().containsKey("RHO")) {
                if (!silent) {
                    BootstrapLogger.standalone(LOGGER, "Overriding configuration from environment variable RHO");
                }

                // overrides from RHO env var
                confMap = overrideConfiguration(confMap, overrides(System.getenv().get("RHO"), true, silent), silent);
            }

            return new Configuration(confMap, confFilePath, silent);
        }
    }

    /**
     * Converts a JSON configuration override file into RHO syntax.
     * 
     * <p>This method transforms a JSON object containing configuration overrides into
     * the RHO format string. Each key-value pair in the JSON is converted to the
     * format: key->value with pairs separated by semicolons.</p>
     * 
     * <h3>Example Conversion</h3>
     * <p>Input JSON:</p>
     * <pre>{@code
     * {
     *   "/logging/log-level": "INFO",
     *   "/core/name": "foo",
     *   "/http-listener/port": 8080
     * }
     * }</pre>
     * <p>Output RHO: {@code /logging/log-level->"INFO";/core/name->"foo";/http-listener/port->8080}</p>
     * 
     * @param jsonReader reader containing the JSON configuration overrides
     * @return RHO format string with semicolon-separated key->value pairs
     * @throws JsonParseException if the JSON is invalid or not an object
     */
    private static String fromJsonToRho(final Reader jsonReader) throws JsonParseException {
        final var gson = new GsonBuilder().setStrictness(Strictness.LENIENT).create(); // setStrictness(Strictness.LENIENT) allows JSON with comments
        final var _json = gson.fromJson(jsonReader, JsonObject.class);

        if (_json == null || !_json.isJsonObject()) {
            throw new JsonParseException("json is not an object");
        }

        final var obj = _json.getAsJsonObject();

        return obj.entrySet().stream()
                .map(e -> e.getKey() + "->" + e.getValue().toString())
                .collect(Collectors.joining(";"));
    }

    /**
     * Converts a YAML configuration override file into RHO syntax.
     * 
     * <p>This method transforms a YAML map containing configuration overrides into
     * the RHO format string. Each key-value pair in the YAML is converted to the
     * format: key->value with pairs separated by semicolons. Values are JSON-encoded
     * to preserve their types.</p>
     * 
     * <h3>Example Conversion</h3>
     * <p>Input YAML:</p>
     * <pre>{@code
     * /logging/log-level: "INFO"
     * /core/name: "foo"
     * /http-listener/port: 8080
     * }</pre>
     * <p>Output RHO: {@code /logging/log-level->"INFO";/core/name->"foo";/http-listener/port->8080}</p>
     * 
     * @param yml reader containing the YAML configuration overrides
     * @return RHO format string with semicolon-separated key->value pairs
     * @throws JsonParseException if the YAML is invalid or cannot be parsed
     */
    private static String fromYmlToRho(final Reader yml) throws JsonParseException {
        final Map<String, Object> _yml = new Yaml(new SafeConstructor(new LoaderOptions())).load(yml);

        if (_yml == null) {
            throw new JsonParseException("json is not an object");
        }

        final var gson = new GsonBuilder().serializeNulls().create();

        return _yml.entrySet().stream()
                .map(e -> e.getKey() + "->" + gson.toJson(e.getValue()))
                .collect(Collectors.joining(";"));
    }

    /**
     * Applies configuration overrides to the configuration map.
     * 
     * <p>This method processes a list of configuration overrides and applies them to
     * the configuration map using JXPath. It handles the creation of nested structures
     * as needed and logs the applied overrides while masking sensitive data.</p>
     * 
     * <h3>Security Features</h3>
     * <ul>
     *   <li>Masks values for keys containing "password", "pwd", or "secret"</li>
     *   <li>Hides passwords in MongoDB connection strings</li>
     *   <li>Shows masked values as "**********" in logs</li>
     * </ul>
     * 
     * <h3>Path Creation</h3>
     * <p>If a path doesn't exist in the configuration, this method creates the necessary
     * parent objects as HashMaps to ensure the override can be applied.</p>
     * 
     * @param confMap the configuration map to override
     * @param overrides list of overrides to apply
     * @param silent if true, suppresses log messages
     * @return the modified configuration map with overrides applied
     */
    private static Map<String, Object> overrideConfiguration(final Map<String, Object> confMap, final List<RhOverride> overrides,
            final boolean silent) {
        final var ctx = JXPathContext.newContext(confMap);
        ctx.setLenient(true);

        // this logs the overrides trying to mask sensite data
        // sensitive data is any key containins "password", "pwd" or "secret"
        // it also hides the password in MongoDB connection string
        // works also if the value is a Json object, checking the root keys (does not
        // hide nested properties)
        overrides.stream().forEachOrdered(o -> {
            if (!silent) {
                if (o.value() instanceof final HashMap<?, ?> mapValue) {
                    final var maskedValue = new HashMap<String, Object>();
                    mapValue.keySet().stream()
                            .filter(k -> k instanceof String)
                            .map(k -> (String) k)
                            .forEach(k -> {
                                if (k.contains("password") || k.contains("pwd") || k.contains("secret")
                                        || k.contains("key")) {
                                    maskedValue.put(k, MASK);
                                } else if (k.contains("connection-string")) {
                                    try {
                                        final var svalue = mapValue.get(k).toString();
                                        final var cs = new ConnectionString(svalue);
                                        final var _pwd = cs.getPassword();
                                        if (_pwd != null) {
                                            final var pwd = new String(_pwd);
                                            maskedValue.put(k, svalue.replaceFirst(Pattern.quote(pwd), MASK));
                                        }
                                    } catch (final Throwable t) {
                                        maskedValue.put(k, mapValue);
                                    }
                                }
                            });
                    LOGGER.info(LOG_PATTERN, o.path(), maskedValue);
                } else if (o.path().contains("password") || o.path().contains("pwd") || o.path().contains("secret")
                        || o.path().contains("key")) {
                    LOGGER.info(LOG_PATTERN, o.path(), MASK);
                } else if (o.path().endsWith("connection-string") && o.value() instanceof final String svalue) {
                    try {
                        final var cs = new ConnectionString(svalue);
                        final var _pwd = cs.getPassword();
                        if (_pwd != null) {
                            final var pwd = new String(_pwd);
                            LOGGER.info(LOG_PATTERN, o.path(), svalue.replaceFirst(Pattern.quote(pwd), MASK));
                        }
                    } catch (final Throwable t) {
                        LOGGER.info(LOG_PATTERN, o.path(), o.value());
                    }
                } else {
                    LOGGER.info(LOG_PATTERN, o.path(), o.value());
                }
            }

            if (!o.path().startsWith("/")) {
                LOGGER.error("Wrong configuration override {}, path must start with /", o.raw());
            } else {
                try {
                    createPathAndSetValue(ctx, o.path(), o.value());
                } catch (final Throwable ise) {
                    LOGGER.error("Wrong configuration override {}, {}", o.raw(), ise.getMessage());
                }
            }
        });

        return confMap;
    }

    /**
     * Creates a path in the configuration context and sets the value.
     * 
     * <p>Uses JXPath to navigate and create nested structures as needed.</p>
     * 
     * @param ctx the JXPath context wrapping the configuration
     * @param path the path to create (e.g., "/core/name")
     * @param value the value to set at the path
     */
    private static void createPathAndSetValue(final JXPathContext ctx, final String path, final Object value) {
        createParents(ctx, path);
        ctx.createPathAndSetValue(path, value);
    }

    /**
     * Creates parent objects for a given path if they don't exist.
     * 
     * <p>Ensures all parent objects in a path exist as Maps before setting
     * a value. For example, for path "/a/b/c", creates objects "a" and "b"
     * if they don't exist.</p>
     * 
     * @param ctx the JXPath context wrapping the configuration
     * @param path the full path whose parents need to be created
     */
    private static void createParents(final JXPathContext ctx, final String path) {
        final var parentPath = path.substring(0, path.lastIndexOf("/"));

        if (!parentPath.equals("")) {
            createParents(ctx, parentPath);
        }

        final var array = path.strip().endsWith("]");

        if (array) {
            // /a/b[2] -> /a/b
            final var arrayPath = path.substring(0, path.lastIndexOf("["));
            if (ctx.getValue(arrayPath) == null) {
                ctx.createPathAndSetValue(arrayPath, new ArrayList<>());
            }
        } else {
            if (ctx.getValue(path) == null) {
                ctx.createPathAndSetValue(path, Maps.newLinkedHashMap());
            }
        }
    }
}
